/**
 * 
 */
package com.netflexitysolutions.amazonws.sdb.orm.internal.operations;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.netflexitysolutions.amazonws.sdb.Item;
import com.netflexitysolutions.amazonws.sdb.ItemAttribute;
import com.netflexitysolutions.amazonws.sdb.SimpleDBException;
import com.netflexitysolutions.amazonws.sdb.orm.SimpleDBSession;
import com.netflexitysolutions.amazonws.sdb.orm.filter.Predicate;
import com.netflexitysolutions.amazonws.sdb.orm.filter.Predicates;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.AttributeMetadata;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.IdMetadata;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.ItemMetadata;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.ManyToManyMetadata;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.ManyToOneMetadata;
import com.netflexitysolutions.amazonws.sdb.orm.metadata.OneToManyMetadata;

/**
 * @author netflexity
 *
 */
public class SimpleDBItemMapper <T> implements ItemMapper<T> {

	private ItemMetadata itemMetadata;
	private SimpleDBSession session;
	private T oneToManyParent;
	
	/**
	 * @param session
	 * @param itemMetadata
	 */
	public SimpleDBItemMapper(SimpleDBSession session, ItemMetadata itemMetadata) {
		super();
		this.session = session;
		this.itemMetadata = itemMetadata;
	}

	/* (non-Javadoc)
	 * @see com.netflexitysolutions.amazonws.sdb.orm.internal.operations.ItemMapper#mapItem(com.xerox.amazonws.sdb.Item)
	 */
	public T mapItem(Item item) throws SimpleDBException{
		T record = (T) itemMetadata.newRecord();
		
		// Get all item attributes.
		List<ItemAttribute> itemAttributes = item.getAttributes();
		
		// Set Id.
		setId(item, record);
		
		// Set attributes.
		setAttributes(item, record, itemAttributes);
		
		// Get all many-to-ones.
		setManyToOne(record, itemAttributes);
		
		// Set one-to-many.
		setOneToMany(record);
		
		// Set many-to-many.
		setManyToMany(record, itemAttributes);
		
		return record;
	}
	
	/**
	 * @param item
	 * @param record
	 * @throws SimpleDBException
	 */
	public void setId(Item item, T record) throws SimpleDBException{
		IdMetadata idMetadata = itemMetadata.getId();
		if(idMetadata != null){
			idMetadata.fromSimpleDB(record, item.getIdentifier());
		}
	}

	/**
	 * @param item
	 * @param record
	 * @param itemAttributes
	 * @throws SimpleDBException
	 */
	public void setAttributes(Item item, T record, List<ItemAttribute> itemAttributes) throws SimpleDBException{
		for (ItemAttribute itemAttribute : itemAttributes) {
			String columnName = itemAttribute.getName();
			AttributeMetadata attributeMetadata = itemMetadata.getAttributeMetadataByColumn(columnName);
			if(attributeMetadata != null){
				attributeMetadata.fromSimpleDB(record, itemAttribute.getValue());
			}
		}
	}
	
	/**
	 * Note that this method must be called after ID is set.
	 * 
	 * @param item
	 * @param record
	 * @throws SimpleDBException
	 */
	public void setOneToMany(T record) throws SimpleDBException{
		populateOneToMany(session, record, itemMetadata, false);
	}
		
	/**
	 * @param record
	 * @param itemAttributes
	 * @throws SimpleDBException
	 */
	public void setManyToOne(T record, List<ItemAttribute> itemAttributes) throws SimpleDBException{
		for (ItemAttribute itemAttribute : itemAttributes) {
			String columnName = itemAttribute.getName();
			
			// Handle Many-to-One.
			ManyToOneMetadata manyToOneMetadata = itemMetadata.getManyToOneMetadataByColumn(columnName);
			if(manyToOneMetadata != null){
				populateManyToOne(session, record, getOneToManyParent(), itemMetadata, manyToOneMetadata, itemAttribute, false);
			}
		}
	}
	
	/**
	 * @param item
	 * @param record
	 * @param itemAttributes
	 * @throws SimpleDBException
	 */
	public void setManyToMany(T record, List<ItemAttribute> itemAttributes) throws SimpleDBException{
		for (ItemAttribute itemAttribute : itemAttributes) {
			String columnName = itemAttribute.getName();
			
			// Handle Many-to-Many.
			ManyToManyMetadata manyToManyMetadata = itemMetadata.getManyToManyMetadataByColumn(columnName);
			if(manyToManyMetadata != null){
				populateManyToMany(session, record, itemMetadata, manyToManyMetadata, itemAttribute, false);
			}
		}
	}
	
	/**
	 * @param <T>
	 * @param session
	 * @param record
	 * @param itemMetadata
	 * @param forceEager
	 * @throws SimpleDBException
	 */
	public static <T>void populateOneToMany(SimpleDBSession session, T record, ItemMetadata itemMetadata, boolean forceEager) throws SimpleDBException{
		IdMetadata pkMetadata = itemMetadata.getId();
		List<OneToManyMetadata> oneToMany = itemMetadata.getOneToMany();
		for (OneToManyMetadata o2m : oneToMany) {
			// Handle One-to-Many.
			if(o2m != null){
				// Get metadata information for oneToMany holder. It must exist.
				ItemMetadata oneToManyItemMetadata = itemMetadata.getMapping().getItemMetadataByClass(o2m.getJavaClass());
				assert(oneToManyItemMetadata != null);
				
				// Get foreign key info.
				AttributeMetadata foreignKeyMetadata = oneToManyItemMetadata.getManyToOneMetadataByColumn(o2m.getColumnName());
				if(foreignKeyMetadata == null){
					foreignKeyMetadata = oneToManyItemMetadata.getOneToManyMetadataByColumn(o2m.getColumnName());
				}
				if(foreignKeyMetadata == null){
					foreignKeyMetadata = oneToManyItemMetadata.getAttributeMetadataByColumn(o2m.getColumnName());
				}
				assert(foreignKeyMetadata != null);
				
				// Handle loading if not lazy.
				if(!o2m.isLazy() || (o2m.isLazy() && forceEager)){
					// select * from oneToManyItemMetadata.getDomainName() where columnName = itemAttribute.getValue()
					String primaryKeyValue = String.valueOf(pkMetadata.getFieldValue(record));
					Predicate<T> predicate = (Predicate<T>) Predicates.getEqualityPredicate(oneToManyItemMetadata.getJavaClass(), session.getFactory().getMapping(), foreignKeyMetadata.getName(), primaryKeyValue, false);
					Select<T> select = new Select<T>(session, predicate);
					select.setOneToManyParent(record);
					try {
						Collection<T> records;
						List<T> foundBeans = select.call();
						if(o2m.getCollectionClazz().isAssignableFrom(Set.class)){
							records = new LinkedHashSet<T>(foundBeans);
						}
						else{
							records = new ArrayList<T>(foundBeans);
						}
						
						// Set value into this record. Setter must be present.
						o2m.setFieldValue(record, records);
					} 
					catch(SimpleDBException e){
						throw e;
					}
					catch (Exception e) {
						throw new SimpleDBException(e);
					}
					finally{
						// Reset parent not t conflict with many-to-one for the same object.
						select.setOneToManyParent(null);
					}
				}
			}
		}
	}
	
	/**
	 * @param <T>
	 * @param session
	 * @param record
	 * @param oneToManyParent
	 * @param itemMetadata
	 * @param manyToOneMetadata
	 * @param itemAttribute
	 * @param forceEager
	 * @throws SimpleDBException
	 */
	public static <T>void populateManyToOne(SimpleDBSession session, T record, T oneToManyParent, ItemMetadata itemMetadata, ManyToOneMetadata manyToOneMetadata, ItemAttribute itemAttribute, boolean forceEager) throws SimpleDBException{
		// Get metadata information for manyToOne holder. It must exist.
		ItemMetadata manyToOneItemMetadata = itemMetadata.getMapping().getItemMetadataByClass(manyToOneMetadata.getJavaClass());
		assert(manyToOneItemMetadata != null);
		
		// Get primary key info.
		IdMetadata primaryKeyMetadata = manyToOneItemMetadata.getId();
		assert(primaryKeyMetadata != null);
		
		// Get manyToOne holder instance.
		Object manyToOneBean = manyToOneMetadata.getFieldValue(record);
		if(manyToOneBean == null && itemAttribute != null){
			manyToOneBean = manyToOneItemMetadata.newRecord();
			
			// Handle lazy loading by only populating primary key property into manyToOneBean with Amazon Attribute value.
			primaryKeyMetadata.fromSimpleDB(manyToOneBean, itemAttribute.getValue());
			
			// Set value into this record. Setter must be present.
			manyToOneMetadata.setFieldValue(record, manyToOneBean);
		}
		assert(manyToOneBean != null);

		if(!manyToOneMetadata.isLazy() || (manyToOneMetadata.isLazy() && forceEager)){
			// select * from manyToOneItemMetadata.getDomainName() where itemId = itemAttribute.getValue()
			String id = String.valueOf(primaryKeyMetadata.getFieldValue(manyToOneBean));
			
			// This is important to escape infinite circular dependencies.
			if(oneToManyParent == null){
				manyToOneBean = session.find(manyToOneItemMetadata.getJavaClass(), id);
			}
			else{
				manyToOneBean = oneToManyParent;
			}
			
			// Set value into this record. Setter must be present.
			manyToOneMetadata.setFieldValue(record, manyToOneBean);
		}
	}
	
	/**
	 * @param <T>
	 * @param session
	 * @param record
	 * @param itemMetadata
	 * @param manyToManyMetadata
	 * @param itemAttribute
	 * @param forceEager
	 * @throws SimpleDBException
	 */
	public static <T>void populateManyToMany(SimpleDBSession session, T record, ItemMetadata itemMetadata, ManyToManyMetadata manyToManyMetadata, ItemAttribute itemAttribute, boolean forceEager) throws SimpleDBException{
		// Get metadata information for manyToMany holder. It must exist.
		ItemMetadata manyToManyItemMetadata = itemMetadata.getMapping().getItemMetadataByClass(manyToManyMetadata.getJavaClass());
		assert(manyToManyItemMetadata != null);
		
		// Get primary key info.
		IdMetadata primaryKeyMetadata = manyToManyItemMetadata.getId();
		assert(primaryKeyMetadata != null);
		
		Collection<Object> manyToManyBeans = (Collection<Object>)manyToManyMetadata.getFieldValue(record);
		if((manyToManyBeans == null || manyToManyBeans.isEmpty()) && itemAttribute != null){
			List<String> values = itemAttribute.getValues();
			List<String> primaryKeys = new ArrayList<String>(values.size());
			if(manyToManyMetadata.getCollectionClazz().isAssignableFrom(Set.class)){
				manyToManyBeans = new LinkedHashSet<Object>(values.size());
			}
			else{
				manyToManyBeans = new ArrayList<Object>(values.size());
			}
			for (String value : values) {
				// Create manyToMany holder instance.
				Object manyToManyBean = manyToManyItemMetadata.newRecord();
				assert(manyToManyBean != null);
				
				// Handle lazy loading by only populating primary key property into manyToManyBean with Amazon Attribute value.
				primaryKeyMetadata.fromSimpleDB(manyToManyBean, value);
				String id = String.valueOf(primaryKeyMetadata.getFieldValue(manyToManyBean));
				primaryKeys.add(id);
				
				// Add bean with pk to list.
				manyToManyBeans.add(manyToManyBean);
			}
			
			if(!manyToManyMetadata.isLazy() || forceEager){
				// select * from manyToManyItemMetadata.getDomainName() where itemId = itemAttribute.getValue()
				Predicate<T> predicate = Predicates.getInPredicate((Class<T>)manyToManyItemMetadata.getJavaClass(), session.getFactory().getMapping(), primaryKeyMetadata.getName(), primaryKeys.toArray());
				List<T> foundBeans = session.select(predicate, new String[]{});
				if(manyToManyMetadata.getCollectionClazz().isAssignableFrom(Set.class)){
					manyToManyBeans = new LinkedHashSet<Object>(foundBeans);
				}
				else{
					manyToManyBeans = new ArrayList<Object>(foundBeans);
				}
			}
			
			// Set value into this record. Setter must be present.
			manyToManyMetadata.setFieldValue(record, manyToManyBeans);
		}
		else{
			if(manyToManyMetadata.isLazy() && forceEager){
				List<String> primaryKeys = new ArrayList<String>(manyToManyBeans.size());
				for (Object manyToManyBean : manyToManyBeans) {
					// select * from manyToManyItemMetadata.getDomainName() where itemId = itemAttribute.getValue()
					String id = String.valueOf(primaryKeyMetadata.getFieldValue(manyToManyBean));
					primaryKeys.add(id);
				}
				
				Predicate<T> predicate = Predicates.getInPredicate((Class<T>)manyToManyItemMetadata.getJavaClass(), session.getFactory().getMapping(), primaryKeyMetadata.getName(), primaryKeys.toArray());
				List<T> foundBeans = session.select(predicate, new String[]{});
				if(manyToManyMetadata.getCollectionClazz().isAssignableFrom(Set.class)){
					manyToManyBeans = new LinkedHashSet<Object>(foundBeans);
				}
				else{
					manyToManyBeans = new ArrayList<Object>(foundBeans);
				}
				
				// Set value into this record. Setter must be present.
				manyToManyMetadata.setFieldValue(record, manyToManyBeans);
			}
		}
	}
	/**
	 * @return the itemMetadata
	 */
	public ItemMetadata getItemMetadata() {
		return itemMetadata;
	}

	/**
	 * @param itemMetadata the itemMetadata to set
	 */
	public void setItemMetadata(ItemMetadata itemMetadata) {
		this.itemMetadata = itemMetadata;
	}

	/**
	 * @return the session
	 */
	public SimpleDBSession getSession() {
		return session;
	}

	/**
	 * @param session the session to set
	 */
	public void setSession(SimpleDBSession session) {
		this.session = session;
	}

	/**
	 * @return the oneToManyParent
	 */
	public T getOneToManyParent() {
		return oneToManyParent;
	}

	/**
	 * @param oneToManyParent the oneToManyParent to set
	 */
	public void setOneToManyParent(T oneToManyParent) {
		this.oneToManyParent = oneToManyParent;
	}
}
